package com.magnificent.atlantis.client.scheduler;

import com.magnificent.atlantis.client.AtlantisConstants;
import com.magnificent.atlantis.client.data.message.ClientMessageHelper;
import com.magnificent.atlantis.client.data.message.messages.*;
import com.magnificent.atlantis.client.data.task.planner.AbstractPlannerTask;
import com.magnificent.atlantis.client.data.task.planner.CommunityMeetingPlannerTask;
import com.magnificent.atlantis.client.event.EventDispatcher;
import com.magnificent.atlantis.client.event.EventDispatcherListener;
import com.magnificent.atlantis.client.event.EventType;
import com.magnificent.atlantis.client.robot.RobotContext;
import com.magnificent.atlantis.client.scheduler.logger.UserThreadLogger;
import com.magnificent.atlantis.client.ui.planner.PlannerData;
import com.magnificent.commons.utils.DateUtils;
import com.magnificent.commons.utils.TimeConstants;
import com.magnificent.commons.utils.collections.list.MetaArrayList;
import com.magnificent.commons.utils.collections.list.MetaList;
import com.magnificent.commons.utils.collections.list.functions.PredicateFunction;
import com.magnificent.commons.utils.collections.list.functions.SelectFunction;
import com.magnificent.commons.utils.other.Pair;

import java.util.Date;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * User: Babanin
 * Date: 09.08.2009
 * Time: 6:27:56
 */
public class Scheduler extends Thread implements EventDispatcherListener {

    private static final int TASK_INTERMISSION_MIN = 30;

    private UserThreadLogger userThreadLogger;
    private PlannerData plannerData;
    private SchedulerCallBack callBack;
    private RobotContext context;

    private Queue<Pair<EventType, Object>> eventQueue;

    public Scheduler(PlannerData plannerData, SchedulerCallBack callBack, RobotContext context) {
        this.context = context;
        this.callBack = callBack;
        this.plannerData = plannerData;
        this.userThreadLogger = UserThreadLogger.getInstance(context);

        eventQueue = new ConcurrentLinkedQueue<Pair<EventType, Object>>();
        EventDispatcher.getInstance(context).addListener(this);
    }

    public void run() {
        while (true) {
            if (!eventQueue.isEmpty()) {
                final Pair<EventType, Object> eventTypeObjectPair = eventQueue.poll();

                Object obj = eventTypeObjectPair.getSecond();
                EventType eventType = eventTypeObjectPair.getFirst();

                if (EventType.NEW_MESSAGE == eventType) {
                    if (obj instanceof ClientMeetingMessage) {
                        CommunityMeetingPlannerTask meetingPlannerTask = getCommunityTaskByMeetingId((ClientMeetingMessage) obj);

                        if (meetingPlannerTask != null) {
                            if (obj instanceof ClientAcceptMeetingMessage) {
                                meetingPlannerTask.setScheduled(true);
                                meetingPlannerTask.setEndDate(new Date(meetingPlannerTask.getStartDate().getTime() + meetingPlannerTask.getDuration()));

                                userThreadLogger.info("Task planned: [Name = " + meetingPlannerTask.getName() +
                                        "; StartDate=" + AtlantisConstants.DATE_TIME_FORMAT.format(meetingPlannerTask.getStartDate()) +
                                        "; EndDate=" + AtlantisConstants.DATE_TIME_FORMAT.format(meetingPlannerTask.getEndDate()) + "]");

                                EventDispatcher.getInstance(context).postEvent(EventType.TASK_SCHEDULED, meetingPlannerTask);
                            } else if (obj instanceof ClientCancelMeetingMessage) {
                                plannerData.getConfigurationContainer().getDataTasks().remove(meetingPlannerTask);
                                userThreadLogger.error("Meeting task can't planned [Name=" + meetingPlannerTask.getName() +
                                        "; Duration (min)=" + meetingPlannerTask.getDuration() / TimeConstants.MLS_IN_MIN + "]. Invited user canceled task.");
                            } else if (obj instanceof ClientChangeMeetingTimeMessage) {
                                ClientChangeMeetingTimeMessage meetingTimeMessage = (ClientChangeMeetingTimeMessage) obj;
                                if (checkTimeForTask(meetingTimeMessage.getNewMeetingTime(), meetingPlannerTask.getDuration())) {
                                    meetingPlannerTask.setStartDate(meetingTimeMessage.getNewMeetingTime());

                                    ClientAcceptMeetingMessage acceptMeetingMessage = new ClientAcceptMeetingMessage();
                                    acceptMeetingMessage.setMeetingId(meetingPlannerTask.getMeetingId());
//                                    acceptMeetingMessage
                                }
                            }
                        }
                    } else if (obj instanceof ClientNewMeetingMessage) {

                    }
                } else if (EventType.NEW_TASK == eventType) {
                    if (obj instanceof CommunityMeetingPlannerTask) {
                        CommunityMeetingPlannerTask meetingPlannerTask = (CommunityMeetingPlannerTask) obj;
                        Date date = findSuitableTime(meetingPlannerTask.getStartDate(), meetingPlannerTask.getDuration());

                        if (date == null) {
                            userThreadLogger.error("Meeting task can't planned [Name=" + meetingPlannerTask.getName() +
                                    "; Duration (min)=" + meetingPlannerTask.getDuration() / TimeConstants.MLS_IN_MIN + "]. No time for task.");
                            plannerData.getConfigurationContainer().getDataTasks().remove(meetingPlannerTask);
                        } else {
                            ClientNewMeetingMessage newMeetingMessage = ClientMessageHelper.createNewMeeting(meetingPlannerTask);

                            plannerData.getMessagesContainer().addMessage(newMeetingMessage);
                            plannerData.getConfigurationContainer().getDataTasks().add((AbstractPlannerTask) obj);

                            EventDispatcher.getInstance().postEvent(EventType.NEW_MESSAGE, newMeetingMessage);
                        }
                    } else {
                        AbstractPlannerTask task = (AbstractPlannerTask) obj;
                        long startPlanningTime = System.currentTimeMillis();
                        Date startDate = findSuitableTime(task.getStartDate(), task.getDuration());

                        if (startDate == null) {
                            task.setCantBeScheduled(true);
                            userThreadLogger.error("Task can't planned: [Name=" +
                                    task.getName() + "; Duration (min)=" + task.getDuration() / TimeConstants.MLS_IN_MIN + "]");
                        } else {
                            task.setScheduled(true);
                            task.setStartDate(startDate);
                            task.setEndDate(new Date(startDate.getTime() + task.getDuration()));

                            userThreadLogger.info("Task planned: [Name = " + task.getName() +
                                    "; StartDate=" + AtlantisConstants.DATE_TIME_FORMAT.format(task.getStartDate()) +
                                    "; EndDate=" + AtlantisConstants.DATE_TIME_FORMAT.format(task.getEndDate()) +
                                    "] for " + (System.currentTimeMillis() - startPlanningTime) + " mls.");
                            EventDispatcher.getInstance(context).postEvent(EventType.TASK_SCHEDULED, task);
                        }
                    }
                }
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private CommunityMeetingPlannerTask getCommunityTaskByMeetingId(final ClientMeetingMessage message) {
        return new MetaArrayList<AbstractPlannerTask>(plannerData.getConfigurationContainer().getDataTasks()).
                where(new PredicateFunction<AbstractPlannerTask>() {
                    public boolean predicate(AbstractPlannerTask task) {
                        if (task instanceof CommunityMeetingPlannerTask) {
                            CommunityMeetingPlannerTask communityMeetingPlannerTask =
                                    (CommunityMeetingPlannerTask) task;
                            return communityMeetingPlannerTask.getMeetingId() == message.getMeetingId();
                        }

                        return false;
                    }
                }).
                select(new SelectFunction<AbstractPlannerTask, CommunityMeetingPlannerTask>() {
                    public CommunityMeetingPlannerTask select(AbstractPlannerTask abstractPlannerTask) {
                        return (CommunityMeetingPlannerTask) abstractPlannerTask;
                    }
                }).
                singleOrDefault();
    }

    private Date findSuitableTime(Date startDate, long taskDuration) {
        int currentDay = 0;

        while (currentDay < 5) {
            Date startOfWorkDay = new Date(DateUtils.startOfDayInMillis(startDate.getTime()) + currentDay * TimeConstants.MLS_IN_DAY +
                    context.getWorkDayStart() * TimeConstants.MLS_IN_HOUR);
            Date endOfWorkDay = new Date(startOfWorkDay.getTime() + (context.getWorkDayFinish() - context.getWorkDayStart()) *
                    TimeConstants.MLS_IN_HOUR);

            Date prevStartDate = startOfWorkDay;
            MetaList<AbstractPlannerTask> tasks = getTasksByDay(DateUtils.startOfDay(startOfWorkDay));

            for (AbstractPlannerTask task : tasks) {
                if (DateUtils.distance(prevStartDate, task.getStartDate()) > TASK_INTERMISSION_MIN * 2 * TimeConstants.MLS_IN_MIN + taskDuration) {
                    return new Date(startDate.getTime() + TASK_INTERMISSION_MIN * TimeConstants.MLS_IN_MIN);
                } else {
                    prevStartDate = task.getEndDate();
                }
            }

            if (DateUtils.distance(prevStartDate, endOfWorkDay) > TASK_INTERMISSION_MIN * 2 * TimeConstants.MLS_IN_MIN + taskDuration) {
                return new Date(prevStartDate.getTime() + TASK_INTERMISSION_MIN * TimeConstants.MLS_IN_MIN);
            }

            currentDay++;
        }

        return null;
    }

    private boolean checkTimeForTask(Date startDate, long duration) {
        Date endDate = new Date(startDate.getTime() + duration);
        Date startOfWorkDay = new Date(DateUtils.startOfDayInMillis(startDate.getTime()) +
                context.getWorkDayStart() * TimeConstants.MLS_IN_HOUR);
        Date endOfWorkDay = new Date(startOfWorkDay.getTime() + (context.getWorkDayFinish() - context.getWorkDayStart()) *
                TimeConstants.MLS_IN_HOUR);

        Date prevStartDate = startOfWorkDay;
        MetaList<AbstractPlannerTask> tasks = getTasksByDay(DateUtils.startOfDay(startOfWorkDay));
        for (AbstractPlannerTask task : tasks) {
            if (DateUtils.afterDate(prevStartDate, startDate) && DateUtils.beforeDate(endDate, task.getStartDate())) {
                return true;
            }

            prevStartDate = new Date(task.getEndDate().getTime() + TimeConstants.MLS_IN_MIN * 5);
        }

        return DateUtils.afterDate(prevStartDate, startDate) && DateUtils.beforeDate(endDate, endOfWorkDay);

    }

    private MetaList<AbstractPlannerTask> getTasksByDay(final Date taskStartDay) {
        return
                new MetaArrayList<AbstractPlannerTask>(
                        plannerData.getConfigurationContainer().getDataTasks()).where(new PredicateFunction<AbstractPlannerTask>() {
                    public boolean predicate(AbstractPlannerTask abstractPlannerTask) {
                        return taskStartDay.equals(DateUtils.startOfDay(abstractPlannerTask.getStartDate())) &&
                                (abstractPlannerTask.isScheduled() || abstractPlannerTask.isMeetingTask());
                    }
                }).orderBy();
    }


    public void newEvent(EventType eventType, Object obj) {
        eventQueue.add(new Pair<EventType, Object>(eventType, obj));
    }

    public SchedulerCallBack getCallBack() {
        return callBack;
    }

    public UserThreadLogger getLogger() {
        return userThreadLogger;
    }

    public PlannerData getPlannerData() {
        return plannerData;
    }
}
